import { xrplAccountToEvmAddress } from "../util/address-derivation";
import { BigNumber, ethers } from "ethers";
import { BridgeConfig } from "./index";
import { Log, LogStatus, LogType } from "../../util/Logger";
import Safe, { EthersAdapter, SafeFactory } from "@safe-global/protocol-kit";
import crypto from "crypto";
import { XChainBridge } from "xrpl/dist/npm/models/common";
import { BridgeDoorToken__factory } from "@peersyst/xrp-evm-contracts/dist/typechain/factories/BridgeDoorToken__factory";
import { BridgeDoorNative__factory } from "@peersyst/xrp-evm-contracts/dist/typechain/factories/BridgeDoorNative__factory";
import { BridgeChainProvider } from "./BridgeChainProvider";
import { getContractAddress } from "ethers/lib/utils";

export class EvmBridgeChainProvider implements BridgeChainProvider<undefined> {
    toEvmAddress(address: string): string {
        if (address.startsWith("0x")) return address;
        else return xrplAccountToEvmAddress(address);
    }

    defaultSafeContractNetworks(chainId: number) {
        return {
            [chainId.toString()]: {
                safeMasterCopyAddress: "0x3E5c63644E683549055b9Be8653de26E0B4CD36E",
                safeProxyFactoryAddress: "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
                multiSendAddress: "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761",
                multiSendCallOnlyAddress: "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D",
                fallbackHandlerAddress: "0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4",
                signMessageLibAddress: "0x03F886722b44BefB13871D4a05621D38616D3b7c",
                createCallAddress: "0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4",
            },
        };
    }

    createBridgeWallet(): undefined {
        return undefined;
    }

    async fundAddress(fundingWallet: ethers.Wallet, address: string, amount: number): Promise<void> {
        const result = await fundingWallet.sendTransaction({
            from: fundingWallet.address,
            to: address,
            value: ethers.utils.parseEther(amount.toString()),
            gasLimit: 300_000,
        });
        await result.wait();
    }

    async setupAccounts(config: BridgeConfig, signer: ethers.Wallet, witnesses: string[]): Promise<string> {
        Log(LogType.Bridge, LogStatus.ToDo, "Funding witnesses...");
        for (const witness of witnesses) {
            Log(LogType.Bridge, LogStatus.Working, `Funding witness ${witness}...`);
            await this.fundAddress(signer, witness, 1);
        }
        Log(LogType.Bridge, LogStatus.Done, "Witnesses funded");

        const bridgeAddress = getContractAddress({
            from: signer.address,
            nonce:
                (await signer.getTransactionCount()) +
                2 +
                (witnesses.find((w) => w.toLowerCase() === signer.address.toLowerCase()) ? 0 : 1),
        });
        Log(LogType.Bridge, LogStatus.ToDo, `Setting up safe smart contract [predicted bridge address ${bridgeAddress}]...`);
        const safeAddress = await this.setupSafe(config, bridgeAddress, signer, witnesses);
        Log(LogType.Bridge, LogStatus.Done, `Safe smart contract set up at ${safeAddress}`);

        return safeAddress;
    }

    async setupSafe(config: BridgeConfig, bridgeAddress: string, signer: ethers.Wallet, witnesses: string[]): Promise<string> {
        const ethAdapter: any = new EthersAdapter({ ethers, signerOrProvider: signer });
        const safeFactory = await SafeFactory.create({
            ethAdapter,
            contractNetworks: this.defaultSafeContractNetworks(await signer.getChainId()),
        });

        const isSignerWitness = !!witnesses.find((w) => w.toLowerCase() === signer.address.toLowerCase());

        const safe: Safe = await safeFactory.deploySafe({
            safeAccountConfig: {
                threshold: 1,
                owners: isSignerWitness ? witnesses : [...witnesses, signer.address],
            },
            saltNonce: BigNumber.from(crypto.randomInt(1_000_000)).toString(),
            options: {
                gasLimit: 300_000,
            },
        });
        const enableModuleTransaction = await safe.createEnableModuleTx(bridgeAddress);
        const safeTxHash = await safe.getTransactionHash(enableModuleTransaction);
        const signature = await safe.signTransactionHash(safeTxHash);
        enableModuleTransaction.addSignature(signature);
        await safe.executeTransaction(enableModuleTransaction);

        if (!isSignerWitness) {
            const removeSignerTx = await safe.createRemoveOwnerTx({
                ownerAddress: signer.address,
                threshold: config.threshold,
            });
            const removeSignerTxHash = await safe.getTransactionHash(removeSignerTx);
            const signature = await safe.signTransactionHash(removeSignerTxHash);
            removeSignerTx.addSignature(signature);
            await safe.executeTransaction(removeSignerTx);
        }

        return safe.getAddress();
    }

    async createIssuingChainBridge(
        config: BridgeConfig,
        lockingBridgeAddress: string,
    ): Promise<{ address: string; tokenAddress?: string; config: XChainBridge }> {
        const provider = new ethers.providers.JsonRpcProvider(config.issuingChain.url);
        const signerWallet = new ethers.Wallet(config.issuingChain.fundingPrivateKey, provider);

        const safeAddress = await this.setupAccounts(config, signerWallet, config.issuingChain.witnesses);
        let bridgeAddress, tokenAddress;

        const isNative = config.lockingChain.tokenCode === "XRP";
        if (isNative) {
            Log(LogType.Bridge, LogStatus.ToDo, `Setting up native bridge smart contract ${lockingBridgeAddress}`);
            const bridgeDoorNativeFactory = new BridgeDoorNative__factory(signerWallet);
            const bridgeDoorNative = await bridgeDoorNativeFactory.deploy(
                safeAddress,
                ethers.utils.parseEther(config.minRewardAmount.toString()),
                ethers.utils.parseEther(config.minCreateAmount.toString()),
                ethers.constants.AddressZero,
                this.toEvmAddress(lockingBridgeAddress),
            );
            Log(LogType.Bridge, LogStatus.Done, `Bridge smart contract set up at ${bridgeDoorNative.address}`);
            bridgeAddress = bridgeDoorNative.address;
        } else {
            Log(
                LogType.Bridge,
                LogStatus.ToDo,
                `Setting up bridge smart contract ${lockingBridgeAddress} ${config.lockingChain.tokenIssuer}...`,
            );
            const bridgeDoorTokenFactory = new BridgeDoorToken__factory(signerWallet);
            const bridgeDoorToken = await bridgeDoorTokenFactory.deploy(
                safeAddress,
                ethers.utils.parseEther(config.minRewardAmount.toString()),
                this.toEvmAddress(lockingBridgeAddress),
                this.toEvmAddress(config.lockingChain.tokenIssuer),
                ethers.constants.AddressZero,
                ethers.constants.AddressZero,
                config.lockingChain.tokenCode,
                `Bridged ${config.lockingChain.tokenCode} (${config.lockingChain.tokenIssuer})`,
            );
            Log(LogType.Bridge, LogStatus.Done, `Bridge smart contract set up at ${bridgeDoorToken.address}`);

            bridgeAddress = bridgeDoorToken.address;
            tokenAddress = await bridgeDoorToken._tokenAddress();
        }

        return {
            address: bridgeAddress,
            config: {
                LockingChainDoor: this.toEvmAddress(lockingBridgeAddress),
                LockingChainIssue: {
                    currency: config.lockingChain.tokenCode,
                    issuer: !isNative ? this.toEvmAddress(config.lockingChain.tokenIssuer) : "",
                },
                IssuingChainDoor: bridgeAddress,
                IssuingChainIssue: {
                    currency: config.lockingChain.tokenCode,
                    issuer: !isNative ? bridgeAddress : "",
                },
            },
            tokenAddress: tokenAddress,
        };
    }

    async createLockingChainBridge(
        config: BridgeConfig,
        issuingBridgeAddress: string,
    ): Promise<{ address: string; tokenAddress?: string; config: XChainBridge }> {
        const provider = new ethers.providers.JsonRpcProvider(config.lockingChain.url);
        const signerWallet = new ethers.Wallet(config.lockingChain.fundingPrivateKey, provider);

        const safeAddress = await this.setupAccounts(config, signerWallet, config.lockingChain.witnesses);

        const bridgeDoorTokenFactory = new BridgeDoorToken__factory(signerWallet);
        const bridgeDoorToken = await bridgeDoorTokenFactory.deploy(
            safeAddress,
            ethers.utils.parseEther(config.minRewardAmount.toString()),
            ethers.constants.AddressZero,
            config.lockingChain.tokenIssuer,
            this.toEvmAddress(issuingBridgeAddress),
            this.toEvmAddress(issuingBridgeAddress),
            config.lockingChain.tokenCode,
            "",
        );

        return {
            address: bridgeDoorToken.address,
            config: {
                LockingChainDoor: bridgeDoorToken.address,
                LockingChainIssue: {
                    currency: config.lockingChain.tokenCode,
                    issuer: config.lockingChain.tokenIssuer,
                },
                IssuingChainDoor: this.toEvmAddress(issuingBridgeAddress),
                IssuingChainIssue: {
                    currency: config.lockingChain.tokenCode,
                    issuer: this.toEvmAddress(issuingBridgeAddress),
                },
            },
        };
    }
}
